<!-- 
  <div>
    <SpringVue
      :width="100"
      :height="180"
      :spring-length="50"
      :base-width="100"
      :base-height="18"
      label="我的球"
      img="./b_b48751c55ab8e36bfa3704af42bd15be.jpg"
    />
  </div> -->
<template>
  <div 
    ref="container"
    class="spring360-container"
    :style="containerStyle"
  >
    <svg
      ref="svg"
      class="spring360-svg"
      :width="width"
      :height="height"
    >
      <path
        :d="springPath"
        stroke="#888"
        stroke-width="1"
        fill="none"
      />
    </svg>
    <div
      ref="ball"
      class="spring360-ball"
      :style="ballStyle"
      @mousedown="onMouseDown"
      @touchstart="onTouchStart"
    >
      <img
        v-if="img"
        :src="img"
        :alt="label"
        class="ball-image"
      >
      <span v-else class="ball-label">{{ label }}</span>
    </div>
    <div
      ref="base"
      class="spring360-base"
      @mousedown="initContainerDrag"
      @touchstart="initContainerDrag"
    />
  </div>
</template>

<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue'

const props = defineProps({
  width: {
    type: Number,
    default: 400
  },
  height: {
    type: Number,
    default: 480
  },
  springLength: {
    type: Number,
    default: 70
  },
  baseWidth: {
    type: Number,
    default: 100
  },
  baseHeight: {
    type: Number,
    default: 18
  },
  label: {
    type: String,
    default: '物体'
  },
  img: {
    type: String,
    default: null
  },
  position: {
    type: String,
    default: 'fixed-br'
  }
})

// 响应式数据
const container = ref(null)
const ball = ref(null)
const base = ref(null)
const svg = ref(null)
const springPath = ref('')
const dragging = ref(false)
const animating = ref(false)
const offset = ref({ x: 0, y: 0 })
const pos = ref({ x: 0, y: 0 })
const vx = ref(0)
const vy = ref(0)

// 计算属性
const springWidth = 4
const springHeight = computed(() => Math.abs(props.height - props.springLength))
const rest = computed(() => ({ 
  x: props.width / 2, 
  y: springHeight.value 
}))
const basePos = computed(() => ({ 
  x: props.width / 2, 
  y: props.height - props.baseHeight 
}))
const turns = 7
const stiffness = 0.96
const damping = 0.966
const radius = 300

const containerStyle = computed(() => ({
  width: `${props.width}px`,
  height: `${props.height}px`,
  position: props.position === 'fixed-br' ? 'fixed' : 'relative',
  ...(props.position === 'fixed-br' ? {
    right: '17%',
    bottom: '13%'
  } : {
    margin: '0 auto'
  })
}))

const ballStyle = computed(() => ({
  width: `${props.springLength * 2}px`,
  height: `${props.springLength * 2}px`,
  left: `${pos.value.x}px`,
  top: `${pos.value.y}px`,
  background: props.img ? 'none' : 'rgba(235, 235, 235, 0.64)'
}))

// 方法
const drawSpring = (p) => {
  const bx = basePos.value.x
  const by = basePos.value.y
  const tx = p.x
  const ty = p.y
  const amp = springWidth
  const dx = bx - tx
  const dy = by - ty
  const angle = Math.atan2(dy, dx)
  const points = []

  for (let i = 0; i <= turns * 2; ++i) {
    const t = i / (turns * 2)
    let px = tx + dx * t
    let py = ty + dy * t
    const perpAngle = angle + Math.PI / 2
    const offset = (i % 2 === 0 ? -amp : amp)
    px += Math.cos(perpAngle) * offset
    py += Math.sin(perpAngle) * offset
    points.push([px, py])
  }

  let d = `M${tx},${ty}`
  points.forEach(([x, y]) => d += ` L${x},${y}`)
  d += ` L${bx},${by}`
  springPath.value = d
}

const moveBall = (clientX, clientY) => {
  let nx = clientX - offset.value.x
  let ny = clientY - offset.value.y
  let cx = nx + props.springLength / 2
  let cy = ny + props.springLength / 2
  let dx = cx - basePos.value.x
  let dy = cy - basePos.value.y
  let dist = Math.sqrt(dx * dx + dy * dy)

  if (dist > radius) {
    dx = dx * radius / dist
    dy = dy * radius / dist
    cx = basePos.value.x + dx
    cy = basePos.value.y + dy
    nx = cx - props.springLength / 2
    ny = cy - props.springLength / 2
  }

  pos.value = { x: nx, y: ny }
  drawSpring(pos.value)
}

const releaseBall = () => {
  const dx = pos.value.x - rest.value.x
  const dy = pos.value.y - rest.value.y
  vx.value = dx * 0.17
  vy.value = dy * 0.17
  animating.value = true
  springBack()
}

const springBack = () => {
  if (!animating.value) return

  let x = pos.value.x
  let y = pos.value.y
  let fx = -stiffness * (x - rest.value.x)
  let fy = -stiffness * (y - rest.value.y)
  vx.value += fx
  vy.value += fy
  vx.value *= damping
  vy.value *= damping
  x += vx.value
  y += vy.value

  let cx = x + props.springLength / 2
  let cy = y + props.springLength / 2
  let dx = cx - basePos.value.x
  let dy = cy - basePos.value.y
  let dist = Math.sqrt(dx * dx + dy * dy)

  if (dist > radius) {
    dx = dx * radius / dist
    dy = dy * radius / dist
    cx = basePos.value.x + dx
    cy = basePos.value.y + dy
    x = cx - props.springLength / 2
    y = cy - props.springLength / 2
    vx.value = 0
    vy.value = 0
  }

  pos.value = { x, y }
  drawSpring(pos.value)

  if (Math.abs(vx.value) < 0.35 && Math.abs(vy.value) < 0.35 && 
      Math.abs(x - rest.value.x) < 0.7 && Math.abs(y - rest.value.y) < 0.7) {
    pos.value = { ...rest.value }
    drawSpring(rest.value)
    animating.value = false
    return
  }

  requestAnimationFrame(springBack)
}

// 事件处理
const onMouseDown = (e) => {
  dragging.value = true
  offset.value.x = e.clientX - pos.value.x
  offset.value.y = e.clientY - pos.value.y
  document.body.style.userSelect = 'none'
  animating.value = false
}

const onMouseMove = (e) => {
  if (!dragging.value) return
  moveBall(e.clientX, e.clientY)
}

const onMouseUp = () => {
  if (dragging.value) {
    dragging.value = false
    document.body.style.userSelect = ''
    releaseBall()
  }
}

const onTouchStart = (e) => {
  if (e.touches.length > 1) return
  e.preventDefault()
  const touch = e.touches[0]
  dragging.value = true
  offset.value.x = touch.clientX - pos.value.x
  offset.value.y = touch.clientY - pos.value.y
  document.body.style.userSelect = 'none'
  animating.value = false
}

const onTouchMove = (e) => {
  if (!dragging.value || e.touches.length > 1) return
  e.preventDefault()
  const touch = e.touches[0]
  moveBall(touch.clientX, touch.clientY)
}

const onTouchEnd = () => {
  if (dragging.value) {
    dragging.value = false
    document.body.style.userSelect = ''
    releaseBall()
  }
}

const initContainerDrag = (e) => {
  if (props.position !== 'fixed-br') return

  let containerDragging = false
  let startX = 0
  let startY = 0
  let originLeft = 0
  let originTop = 0

  const getLeftTop = () => {
    let left = container.value.style.left ? parseFloat(container.value.style.left) : 0
    let top = container.value.style.top ? parseFloat(container.value.style.top) : 0

    if (!container.value.style.left || !container.value.style.top) {
      const rect = container.value.getBoundingClientRect()
      left = rect.left
      top = rect.top
      container.value.style.left = left + "px"
      container.value.style.top = top + "px"
      container.value.style.right = ''
      container.value.style.bottom = ''
    }
    return { left, top }
  }

  const handleStart = (e) => {
    if (e.button !== 0 && !e.touches) return
    containerDragging = true
    document.body.style.userSelect = 'none'
    const { left, top } = getLeftTop()
    startX = e.clientX || e.touches[0].clientX
    startY = e.clientY || e.touches[0].clientY
    originLeft = left
    originTop = top
  }

  const handleMove = (e) => {
    if (!containerDragging) return
    e.preventDefault()
    const clientX = e.clientX || e.touches[0].clientX
    const clientY = e.clientY || e.touches[0].clientY
    const dx = clientX - startX
    const dy = clientY - startY
    container.value.style.left = (originLeft + dx) + "px"
    container.value.style.top = (originTop + dy) + "px"
    container.value.style.right = ''
    container.value.style.bottom = ''
  }

  const handleEnd = () => {
    if (!containerDragging) return
    containerDragging = false
    document.body.style.userSelect = ''
  }

  handleStart(e)
  document.addEventListener('mousemove', handleMove)
  document.addEventListener('mouseup', handleEnd)
  document.addEventListener('touchmove', handleMove)
  document.addEventListener('touchend', handleEnd)

  onUnmounted(() => {
    document.removeEventListener('mousemove', handleMove)
    document.removeEventListener('mouseup', handleEnd)
    document.removeEventListener('touchmove', handleMove)
    document.removeEventListener('touchend', handleEnd)
  })
}

// 生命周期钩子
onMounted(() => {
  pos.value = { ...rest.value }
  drawSpring(rest.value)
  document.addEventListener('mousemove', onMouseMove)
  document.addEventListener('mouseup', onMouseUp)
  document.addEventListener('touchmove', onTouchMove)
  document.addEventListener('touchend', onTouchEnd)
})

onUnmounted(() => {
  document.removeEventListener('mousemove', onMouseMove)
  document.removeEventListener('mouseup', onMouseUp)
  document.removeEventListener('touchmove', onTouchMove)
  document.removeEventListener('touchend', onTouchEnd)
})
</script>

<style scoped>
.spring360-container {
  background: none;
  display: flex;
  justify-content: center;
  align-items: center;
}

.spring360-ball {
  border-radius: 50%;
  position: absolute;
  display: flex;
  justify-content: center;
  align-items: center;
  cursor: grab;
  user-select: none;
  z-index: 10;
  outline: none;
  transform: translate(-50%, -100%);
}

.spring360-ball:active {
  cursor: grabbing;
}

.ball-image {
  position: absolute;
  left: 50%;
  top: 100%;
  transform: translate(-50%, -100%);
  width: auto;
  height: auto;
  max-width: 80%;
  max-height: 80%;
  user-select: none;
  pointer-events: none;
}

.ball-label {
  font-weight: bold;
  font-size: 1.1em;
  color: #04709bff;
  user-select: none;
  pointer-events: none;
}

.spring360-svg {
  position: absolute;
  pointer-events: none;
}

.spring360-base {
  background: #bbb;
  border-radius: 9px;
  position: absolute;
  left: 50%;
  bottom: 0;
  transform: translateX(-50%);
  z-index: 2;
  box-shadow: 0 2px 10px rgba(0,0,0,0.08);
  cursor: move;
}
</style>
